Repository navigation
Conversation
… removed Removing a preset that is the base (or a middle layer) of another preset's prepend/append/wrap composition left the removed preset's content in the active agent's SKILL.md (Claude, Codex, Copilot with --skills, ...), while command-file agents such as Gemini were recomposed onto the new stack. The composed skill is owned by the surviving preset at the top of the chain, so _unregister_skills() restores nothing for the removed preset, and _reconcile_skills() only revisited the active skills directory when that restore had touched it. The recomposed .composed/<cmd>.md that _reconcile_composed_commands() writes was therefore never rendered into the skill, and `specify preset resolve` disagreed with what the agent ran. After a removal, also reconcile active-directory skills for commands whose winning layer composes, as long as the skill still carries the winning preset's source marker. A hand-edited skill is left alone, matching the existing protection for non-owned skills during removal. Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Removal can overwrite a user-owned legacy skill despite the ownership filter.
1 open issue
What changed in this PR
Updates Spec Kit preset removal so surviving composed skills reflect the remaining preset layers.
Changes:
- Re-renders active composed skills that retain the winning preset’s ownership marker.
- Adds regression coverage for base/middle removal and preservation of non-owned skills.
Validation used an isolated ownership check; project tests were not run.
AI disclosure: GitHub Copilot (model: unknown, autonomous) generated this review for the requesting reviewer.
| File | Description |
|---|---|
| tests/specify_cli/presets/test_manager_skills.py | Adds composition-removal and preservation tests. |
| src/specify_cli/presets/_manager_skills.py | Adds ownership-based reconciliation for surviving composed skills. |
Review effort: Max
| active_names |= self._skills_owned_by_preset( | ||
| active_skills_dir, composed_winners | ||
| ) |
There was a problem hiding this comment.
Confirmed and fixed in 8c0328f. _register_skills() now takes an optional only_skill_names filter; on removal the active directory passes its managed names, so a user-owned legacy speckit.specify/SKILL.md next to a recomposed speckit-specify is no longer rewritten. Install and rescaffold pass None and are unchanged. New regression: test_remove_base_preset_preserves_user_owned_legacy_skill, which fails on 1239cbe and passes now.
AI disclosure: posted on behalf of @yxshee by Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode); the fix, test and this reply are AI-generated.
🤖 Addressed by Claude Code
When both the modern (speckit-specify) and legacy (speckit.specify) skill directories exist, the active-directory recomposition added for lower-layer removal selected the command because the modern skill kept the winning preset's marker, and _register_skills() then rewrote both directories, overwriting a user-owned legacy skill. Give _register_skills() an optional only_skill_names filter and pass the directory's managed names on removal, so only names that are still preset-owned are written. Install and rescaffold calls pass None and keep writing every existing directory, as before. Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review round 1, posted on behalf of @yxshee. Addressed the Copilot finding that removal recomposition could overwrite a user-owned legacy skill (commit 8c0328f):
AI disclosure: written and pushed by Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode); the code, test and this comment are AI-generated. 🤖 Addressed by Claude Code |

Description
Removing a preset that is the base, or a middle layer, of another preset's
prepend/append/wrapcomposition leaves the removed preset's content in the active agent'sSKILL.md. That affects Claude, Codex, Copilot with--skills, and other skills-based integrations.specify preset resolvereports the new chain, and command-file agents such as Gemini are recomposed onto it. But the skill keeps the removed preset's instructions, so/speckit-specifyruns content the user just removed.Reproduction (
alphareplacesspeckit.specifywithALPHA BODY;betaappendsBETA APPENDED):The same steps with
--integration geminidropALPHA BODYfrom.gemini/commands/speckit.specify.tomlonmain, so only skills output is stale.Root cause, in
presets/_manager_skills.py:_reconcile_skills()only revisits the active skills directory when_unregister_skills()restored something there (if extra_skills_dirs is None or active_provenance).beta), not the removed one, so nothing is restored and the active directory is skipped..composed/<cmd>.mdthat_reconcile_composed_commands()writes is therefore never rendered into the skill.683bfd0) and moved in the refactor(presets): split domain internals into private modules #4747 refactor. Before fix: register extensions for the active integration only #3459 the active directory was always reconciled.The fix:
_reconcile_skills()now records commands whose winning layer composes (strategy other thanreplace).SKILL.mdstill carries the winning preset'smetadata.source: preset:<id>marker. A new helper,_skills_owned_by_preset(), does that check.Testing
uv run specify --helpuv sync && uv run pytestclaude(onmainand this branch) andgemini(onmain)New tests in
tests/specify_cli/presets/test_manager_skills.py:test_remove_base_preset_recomposes_surviving_composed_skill: fails onmainwithassert "Base preset body" not in contenttest_remove_middle_preset_drops_its_layer_from_composed_skill: fails onmainwithassert "Middle body" not in contenttest_remove_base_preset_preserves_hand_edited_composed_skill: the negative case, guarding the hand-edit protection; passes before and afterWith the change the file gives 70 passed.
tests/test_*_python_parity.pyfiles occur identically on unmodifiedmainon this machine under full-suite load; those files pass when run on their own (257 passed, 112 skipped).test_resolver.py::TestResolveCore::test_resolve_core_does_not_return_preset_files, was an artifact of a worktree directory literally namedpresets: the test asserts"presets" not in path.parts. It fails identically onmainin that directory and passes elsewhere.uvx ruff@0.15.0 check src tests: cleanAI Disclosure
AI disclosure: Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode) opened this PR on behalf of @yxshee. A Claude Code subagent (same model, autonomous) found the bug and wrote the fix and tests. The lead agent independently re-ran the new tests against
mainand the branch, reproduced the behavior with the real CLI (Claude and Gemini), confirmed the #3459 origin withgit log -S, reviewed the diff, and wrote this description. No human reviewed the change line by line before it was opened.🤖 Generated with Claude Code